/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package kassiopeia.agent.log;

import org.apache.log4j.AppenderSkeleton;
import org.apache.log4j.helpers.LogLog;
import org.apache.log4j.spi.ErrorCode;
import org.apache.log4j.spi.LoggingEvent;
import javax.jms.JMSException;
import javax.jms.ObjectMessage;
import javax.jms.Session;
import javax.jms.Topic;
import javax.jms.TopicConnection;
import javax.jms.TopicConnectionFactory;
import javax.jms.TopicPublisher;
import javax.jms.TopicSession;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NameNotFoundException;
import javax.naming.NamingException;
import java.util.Properties;

public class KassiopeiaJMSAppender extends AppenderSkeleton {

    public static final String LOG_TOPIC_NAME = "KASSIOPEIA_LOGS";

    String securityPrincipalName;
    String securityCredentials;
    String initialContextFactoryName;
    String urlPkgPrefixes;
    String providerURL;
    String topicConnectionFactoryName;
    String userName;
    String password;
    boolean locationInfo;

    TopicConnection topicConnection;
    TopicSession topicSession;
    TopicPublisher topicPublisher;

    public KassiopeiaJMSAppender() {
    }


    public String getTopicConnectionFactoryName() {
        return topicConnectionFactoryName;
    }

    public void setTopicConnectionFactoryName(String topicConnectionFactoryName) {
        this.topicConnectionFactoryName = topicConnectionFactoryName;
    }

    /**
     * Returns value of the <b>LocationInfo</b> property which determines
     * whether location (stack) info is sent to the remote subscriber.
     */
    public boolean getLocationInfo() {
        return locationInfo;
    }

    /**
     * Options are activated and become effective only after calling this
     * method.
     */
    public void activateOptions() {
        TopicConnectionFactory topicConnectionFactory;

        try {
            Context jndi;

            LogLog.debug("Getting initial context.");
            if (initialContextFactoryName != null) {
                Properties env = new Properties();
                env.put(Context.INITIAL_CONTEXT_FACTORY, initialContextFactoryName);
                if (providerURL != null) {
                    env.put(Context.PROVIDER_URL, providerURL);
                } else {
                    LogLog.warn("You have set InitialContextFactoryName option but not the "
                            + "ProviderURL. This is likely to cause problems.");
                }
                if (urlPkgPrefixes != null) {
                    env.put(Context.URL_PKG_PREFIXES, urlPkgPrefixes);
                }

                env.put("topic." + LOG_TOPIC_NAME, LOG_TOPIC_NAME);

                if (securityPrincipalName != null) {
                    env.put(Context.SECURITY_PRINCIPAL, securityPrincipalName);
                    if (securityCredentials != null) {
                        env.put(Context.SECURITY_CREDENTIALS, securityCredentials);
                    } else {
                        LogLog.warn("You have set SecurityPrincipalName option but not the "
                                + "SecurityCredentials. This is likely to cause problems.");
                    }
                }
                jndi = new InitialContext(env);
            } else {
                jndi = new InitialContext();
            }

            LogLog.debug("Looking up [" + topicConnectionFactoryName + "]");
            topicConnectionFactory = (TopicConnectionFactory) lookup(jndi, topicConnectionFactoryName);
            LogLog.debug("About to create TopicConnection.");
            if (userName != null) {
                topicConnection = topicConnectionFactory.createTopicConnection(userName, password);
            } else {
                topicConnection = topicConnectionFactory.createTopicConnection();
            }

            LogLog.debug("Creating TopicSession, non-transactional, " + "in AUTO_ACKNOWLEDGE mode.");
            topicSession = topicConnection.createTopicSession(false, Session.AUTO_ACKNOWLEDGE);

            LogLog.debug("Looking up topic name [" + LOG_TOPIC_NAME + "].");
            Topic topic = (Topic) lookup(jndi, LOG_TOPIC_NAME);

            LogLog.debug("Creating TopicPublisher.");
            topicPublisher = topicSession.createPublisher(topic);

            LogLog.debug("Starting TopicConnection.");
            topicConnection.start();

            jndi.close();
        } catch (JMSException e) {
            errorHandler.error("Error while activating options for appender named [" + name + "].", e,
                    ErrorCode.GENERIC_FAILURE);
        } catch (NamingException e) {
            errorHandler.error("Error while activating options for appender named [" + name + "].", e,
                    ErrorCode.GENERIC_FAILURE);
        } catch (RuntimeException e) {
            errorHandler.error("Error while activating options for appender named [" + name + "].", e,
                    ErrorCode.GENERIC_FAILURE);
        }
    }

    protected Object lookup(Context ctx, String name) throws NamingException {
        try {
            return ctx.lookup(name);
        } catch (NameNotFoundException e) {
            LogLog.error("Could not find name [" + name + "].");
            throw e;
        }
    }

    protected boolean checkEntryConditions() {
        String fail = null;

        if (this.topicConnection == null) {
            fail = "No TopicConnection";
        } else if (this.topicSession == null) {
            fail = "No TopicSession";
        } else if (this.topicPublisher == null) {
            fail = "No TopicPublisher";
        }

        if (fail != null) {
            errorHandler.error(fail + " for JMSAppender named [" + name + "].");
            return false;
        } else {
            return true;
        }
    }

    /**
     * Close this JMSAppender. Closing releases all resources used by the
     * appender. A closed appender cannot be re-opened.
     */
    public synchronized void close() {
        // The synchronized modifier avoids concurrent append and close
        // operations

        if (this.closed)
            return;

        LogLog.debug("Closing appender [" + name + "].");
        this.closed = true;

        try {
            if (topicSession != null)
                topicSession.close();
            if (topicConnection != null)
                topicConnection.close();
        } catch (JMSException e) {
            LogLog.error("Error while closing JMSAppender [" + name + "].", e);
        } catch (RuntimeException e) {
            LogLog.error("Error while closing JMSAppender [" + name + "].", e);
        }
        // Help garbage collection
        topicPublisher = null;
        topicSession = null;
        topicConnection = null;
    }

    /**
     * This method called by {@link AppenderSkeleton#doAppend} method to do most
     * of the real appending work.
     */
    public void append(LoggingEvent event) {
        if (!checkEntryConditions()) {
            return;
        }

        try {
            ObjectMessage msg = topicSession.createObjectMessage();
            if (locationInfo) {
                event.getLocationInformation();
            }
            msg.setObject(LogFactory.getLog(event));
            topicPublisher.publish(msg);
        } catch (JMSException e) {
            errorHandler
                    .error("Could not publish message in JMSAppender [" + name + "].", e, ErrorCode.GENERIC_FAILURE);
        } catch (RuntimeException e) {
            errorHandler
                    .error("Could not publish message in JMSAppender [" + name + "].", e, ErrorCode.GENERIC_FAILURE);
        }
    }

    /**
     * Returns the value of the <b>InitialContextFactoryName</b> option. See
     * {@link #setInitialContextFactoryName} for more details on the meaning of
     * this option.
     * */
    public String getInitialContextFactoryName() {
        return initialContextFactoryName;
    }

    /**
     * Setting the <b>InitialContextFactoryName</b> method will cause this
     * <code>JMSAppender</code> instance to use the
     * {@link InitialContext#InitialContext(Hashtable)} method instead of the
     * no-argument constructor. If you set this option, you should also at least
     * set the <b>ProviderURL</b> option.
     *
     * <p>
     * See also {@link #setProviderURL(String)}.
     * */
    public void setInitialContextFactoryName(String initialContextFactoryName) {
        this.initialContextFactoryName = initialContextFactoryName;
    }

    public String getProviderURL() {
        return providerURL;
    }

    public void setProviderURL(String providerURL) {
        this.providerURL = providerURL;
    }

    String getURLPkgPrefixes() {
        return urlPkgPrefixes;
    }

    public void setURLPkgPrefixes(String urlPkgPrefixes) {
        this.urlPkgPrefixes = urlPkgPrefixes;
    }

    public String getSecurityCredentials() {
        return securityCredentials;
    }

    public void setSecurityCredentials(String securityCredentials) {
        this.securityCredentials = securityCredentials;
    }

    public String getSecurityPrincipalName() {
        return securityPrincipalName;
    }

    public void setSecurityPrincipalName(String securityPrincipalName) {
        this.securityPrincipalName = securityPrincipalName;
    }

    public String getUserName() {
        return userName;
    }

    /**
     * The user name to use when
     * {@link TopicConnectionFactory#createTopicConnection(String, String)
     * creating a topic session}. If you set this option, you should also set
     * the <b>Password</b> option. See {@link #setPassword(String)}.
     * */
    public void setUserName(String userName) {
        this.userName = userName;
    }

    public String getPassword() {
        return password;
    }

    /**
     * The paswword to use when creating a topic session.
     */
    public void setPassword(String password) {
        this.password = password;
    }

    /**
     * If true, the information sent to the remote subscriber will include
     * caller's location information. By default no location information is sent
     * to the subscriber.
     */
    public void setLocationInfo(boolean locationInfo) {
        this.locationInfo = locationInfo;
    }

    /**
     * Returns the TopicConnection used for this appender. Only valid after
     * activateOptions() method has been invoked.
     */
    protected TopicConnection getTopicConnection() {
        return topicConnection;
    }

    /**
     * Returns the TopicSession used for this appender. Only valid after
     * activateOptions() method has been invoked.
     */
    protected TopicSession getTopicSession() {
        return topicSession;
    }

    /**
     * Returns the TopicPublisher used for this appender. Only valid after
     * activateOptions() method has been invoked.
     */
    protected TopicPublisher getTopicPublisher() {
        return topicPublisher;
    }

    /**
     * The JMSAppender sends serialized events and consequently does not require
     * a layout.
     */
    public boolean requiresLayout() {
        return false;
    }
}
